import java.io.*;
import java.net.*;
import java.util.*;
import messaging.*;
import debug.*;

public class PrimeClient implements MessageHandler
{
	protected Connection myConnection, serverConnection;
	protected SocketThread server;
	private boolean running;
	private LinkedList<Integer> falsePrimes;
	
	private int myID;
	private int myNumber, maxNumber, currentMultiple;
	private HashElement myEntry;
    
    // power scaling variables
    private static final int MIN_SPEED = 4; // 40 us as maximum sleep
	private static final int SCALE_MAX = 3;  // number of steps
    private static final int MAX_SPEED = 1; // 20 us as minimum sleep
    
	private long startTime;
	private long deadline;

    private int scalingFactor; // 0 -> 25%, 1 -> 50%, 2 -> 75%, 3 -> 100%
	private boolean sleeping;
    private long energy; // in 64ths, done per iteration
    private long numIterations;
    private int sleepTime;
	/**
	* Constructor takes a string representation of the server to connect to.
	*/
	public PrimeClient(String server)
	{
		//Turn debug output off for this server.
		//Debug.setDebug(false);
		try
		{
			Socket socket = new Socket(InetAddress.getByName(server), Server.PORT);
			myConnection = new Connection(socket.getLocalAddress(), socket.getLocalPort());
			serverConnection = new Connection(socket.getInetAddress(), socket.getPort());
			this.server = new SocketThread(socket, this);
		}
		catch(Exception e)
		{
			Debug.println("Could not connect to the Server.");
			System.exit(0);
		}
		Debug.println("Connected to the Server: " + serverConnection.toString());
		running = true;
		
        setSpeed(SCALE_MAX); // start at maximum speed
		sleeping = true;
        energy = 0;
        numIterations = 0;
		startTime = 0;
	}
	
	public void run()
	{
		this.server.start();
	
		int fp;
		while(running)
		{
			//We are sleeping
			if(sleeping)
			{
				try
				{
				Thread.sleep(1);
				} catch (Exception idc){}
			}
			else
			{
				fp = myNumber*(currentMultiple++);
				
				numIterations++;
				energy += (long)(Math.pow(1.0 / (SCALE_MAX - scalingFactor + 1), 3)*64.0);
				
				if(fp <= maxNumber)
				{
					falsePrimes.add(fp);
					try
					{
					Thread.sleep(sleepTime);
					} catch (Exception doh) {}
				}
				else
				{
					submitResults();
				}
			}
		}
	}
	
	public void receive(Message msg)
	{
		//Debug.println(msg.toString());
		switch(msg.getType())
		{
			case INITIALIZE:
				initialize((Integer)msg.getAttachment(0), (Integer)msg.getAttachment(1), (Integer)msg.getAttachment(2));
				break;
			case RESET:
				reset(msg);
				break;
			case SPEED_UP:
				increaseSpeed();
				break;
			case SLOW_DOWN:
				decreaseSpeed();
				break;
			case SET_SPEED:
				setSpeed((Integer)msg.getAttachment(0));
				break;
			case FAIL:
				Debug.println("Missed a deadline, failing" + msg);
				break;
			case LOCK:
				updateHashTable();
				break;
			case FINISHED:
				finish();
				break;
			case HTREMOVE:
				myEntry.remove((Integer)msg.getAttachment(0));
				break;
			default:
				Debug.println("Received a message: \n" + msg);
				break;
		}
	}
	
	//Called to set this client up for calculation
	public void initialize(int myID, int maxNumber, int numClients)
	{
		this.myID = myID;
		this.maxNumber = maxNumber;
		myEntry = new HashElement(numClients);
	}
	
	public void reset(Message msg)
	{
		myNumber = (Integer)msg.getAttachment(0);
		deadline = (Long)msg.getAttachment(1);
		myEntry.addTrial(myNumber);
		currentMultiple = 2;
		falsePrimes = new LinkedList<Integer>();
		sleeping = false;
		numIterations = 0;
		energy = 0;
		startTime = System.currentTimeMillis();
	}
	
	// go faster/slower
	public void setSpeed(int speed) 
	{
		// keep speed value within limits
		scalingFactor = Math.max(0, Math.min(SCALE_MAX, speed));
		
        sleepTime = MAX_SPEED + (MIN_SPEED - MAX_SPEED)*(SCALE_MAX-scalingFactor)/SCALE_MAX;
	}
	
	public void increaseSpeed()
	{
		setSpeed(scalingFactor+1);
	}
    
    public void decreaseSpeed()
	{
		setSpeed(scalingFactor-1);
	}
	
	public void submitResults()
	{
		long timeDiff = System.currentTimeMillis() - startTime;
		Debug.println("Time Diff: " + timeDiff);
		Debug.println("Deadline: " + deadline);
		sleeping = true;
		boolean deadlineMissed = false;
		if(timeDiff > deadline)
		{
			Debug.println("Increasing the speed");
			deadlineMissed = true;
			increaseSpeed();
		}
		else if(timeDiff <= (deadline/3))
		{
			Debug.println("Reducing the speed");
			decreaseSpeed();
		}
		Message resultMsg = new Message(MessageType.UPDATE, server, "Finished calculating all multiples of " + myNumber);
		resultMsg.attach(new Long(energy));
		resultMsg.attach(new Long(numIterations));
		resultMsg.attach(new Long(timeDiff));
		resultMsg.attach(new Boolean(deadlineMissed));
		server.send(resultMsg);
	}
	
	public void updateHashTable()
	{
		//Remove all of the false primes from the distributed list.
		for(Integer i : falsePrimes)
		{
			myEntry.remove(i);
		}
		//Let the server know what the new min probable prime is.
		myEntry.sendMinCandidate();
		server.send(new Message(MessageType.RELEASE, server, "HashTable updated"));
	}
	
	public void sleep(int nanoseconds)
	{
		try
		{
			Thread.sleep(0, nanoseconds);
		}
		catch(InterruptedException e)
		{
			Debug.println("Sleep Interrupted");
		}
	}
	
	synchronized public void finish()
	{
		if(running)
		{
			running = false;
			Message msg = new Message(MessageType.FINISHED, server, "My Primes List");
			Object [] array = myEntry.toArray();
			//System.out.println(Arrays.toString(array));
			for(Object element : array)
			{
				msg.attach((Integer) element);
			}
			server.send(msg);
			System.exit(0);
		}
	}
	
	private class HashElement
	{
		private TreeSet<Integer> dataList;
		private int maxTried;
		private int numClients;
		public HashElement(int numClients)
		{
			this.numClients = numClients;
			dataList = new TreeSet<Integer>();
			maxTried = 0;
			//Add all of the integers that will collide here
			for(int i = myID; i <= maxNumber; i+= numClients)
			{
				dataList.add(i);
			}
			dataList.remove(0);
			dataList.remove(1);
		}
		
		public void remove(int i)
		{
			if((i % numClients) == myID)
			{
				dataList.remove(i);
			}
			else
			{
				Message msg = new Message(MessageType.HTREMOVE, server, "Remove Message from client " + myID);
				msg.attach(i);
				server.send(msg);
			}
		}
		
		public void addTrial(int i)
		{
			maxTried = Math.max(maxTried, i);
		}
		
		public void sendMinCandidate()
		{
			int min = 0;
			for(Integer i : dataList)
			{
				if(i > maxTried)
				{
					min = i;
					break;
				}
			}
			Message msg = new Message(MessageType.HTMIN, server, "My New Minimum Probable Prime: " + min);
			msg.attach(min);
			server.send(msg);
		}
		
		public Object [] toArray()
		{
			return dataList.toArray();
		}
	}
	
	public static void main(String [] args)
	{
		PrimeClient pc = new PrimeClient(args[0]);
		pc.run();
	}
}